package com.piece.core.framework.util.file;

import com.github.junrar.Archive;
import com.github.junrar.rarfile.FileHeader;
import com.piece.core.framework.util.string.JsonUtil;
import com.piece.core.framework.util.string.StringUtil;
import com.piece.core.framework.util.async.AsyncManager;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.Zip64Mode;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
import java.io.*;
import java.math.BigDecimal;
import java.text.CollationKey;
import java.text.Collator;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * .zip,.gzip,.rar,.jar文件工具类
 */
@Slf4j
public class CompressUtil {

    private static Pattern pattern = Pattern.compile("^\\d+");

    /**
     * 文件ZIP压缩
     *
     * @param targetPath 生成压缩包文件地址
     * @param filePath   待压缩文件路径
     */
    public static void zipFile(String targetPath, String filePath) throws IOException {
        FileOutputStream outputStream = null;
        List<InputStream> inputStreams = null;
        List<String> fileNames = null;
        try {
            File zipFileTemp = new File(targetPath);
            String[] files = (new File(filePath)).list();
            outputStream = new FileOutputStream(zipFileTemp);
            inputStreams = new ArrayList<>();
            fileNames = new ArrayList<>();

            for (int i = 0; i < files.length; i++) {
                File f = new File(files[i]);
                inputStreams.add(new FileInputStream(filePath + File.separator + files[i]));
                fileNames.add(f.getName());
            }

            zipFile(inputStreams, fileNames, outputStream);
        } catch (IOException e) {
            log.error("压缩文件IO异常：{}", e.getMessage());
            throw new IOException(e);
        } finally {
            if (null != outputStream) {
                try {
                    outputStream.close();
                } catch (Exception e) {
                    log.error("关闭压缩文件流异常：{}", e.getMessage());
                }
            }
            if (null != inputStreams) {
                inputStreams.forEach(inputStream -> {
                    try {
                        inputStream.close();
                    } catch (Exception e) {
                        log.error("关闭压缩文件流异常：{}", e.getMessage());
                    }
                });
            }
        }
    }

    /**
     * 文件ZIP压缩
     *
     * @param inputStreams 文件流列表
     * @param fileNames    文件名列表
     * @param outputStream 输出流
     */
    public static void zipFile(List<InputStream> inputStreams, List<String> fileNames, OutputStream outputStream) throws IOException {
        ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(outputStream);
        zipArchiveOutputStream.setUseZip64(Zip64Mode.AsNeeded);
        int index = 0;
        for (InputStream inputStream : inputStreams) {
            String fileName = fileNames.get(index++);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                byteArrayOutputStream.write(buffer, 0, len);
            }
            if (null != byteArrayOutputStream) {
                byteArrayOutputStream.flush();
            }
            byte[] bytes = byteArrayOutputStream.toByteArray();

            //设置文件名
            ArchiveEntry entry = new ZipArchiveEntry(fileName);
            zipArchiveOutputStream.putArchiveEntry(entry);
            zipArchiveOutputStream.write(bytes);
            zipArchiveOutputStream.closeArchiveEntry();
            if (null != byteArrayOutputStream) {
                byteArrayOutputStream.close();
            }
        }

        if (null != zipArchiveOutputStream) {
            zipArchiveOutputStream.close();
        }
    }

    /**
     * 读取压缩文件并压缩到指定目录下，文件命名使用压缩文件名+文件名
     *
     * @param srcFile  源文件
     * @param destPath 解压路径
     * @param delete   是否删除源文件
     * @return 解压后路径
     */
    public static String unZipFile(String srcFile, String destPath, boolean delete) {
        try {
            File file = new File(srcFile);
            if (!file.exists()) {
                return null;
            }

            ZipFile zipFile = new ZipFile(file, System.getProperty("file.encoding"));
            Enumeration<ZipArchiveEntry> dir = sortZipEntries(zipFile.getEntries());
            String name = null;
            List<Map<String, ZipArchiveEntry>> entriesToBeExtracted = new ArrayList<>();

            Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
            while (entries.hasMoreElements()){
                ZipArchiveEntry entry = dir.nextElement();
                if (entry.isDirectory()) {
                    name = name.substring(0, name.length() - 1);
                    File fileObject = new File(destPath + File.separator + name);
                    fileObject.mkdirs();
                } else {
                    entriesToBeExtracted.add(Collections.singletonMap(entry.getName(), entry));
                }
            }

            // 开启新的线程处理文件解压
            AsyncManager.getInstance().submit(new ZipExtractorWorker(entriesToBeExtracted, file, zipFile, destPath, delete));
            return destPath;
        } catch (IOException e) {
            log.error("解压zip文件失败：{}", e.getMessage());
            return null;
        }
    }

    /**
     * 按文件路径排序
     */
    private static Enumeration<ZipArchiveEntry> sortZipEntries(Enumeration<ZipArchiveEntry> entries) {
        List<ZipArchiveEntry> sortedEntries = new ArrayList<>();
        while (entries.hasMoreElements()) {
            sortedEntries.add(entries.nextElement());
        }
        Collections.sort(sortedEntries, Comparator.comparingInt(o -> o.getName().length()));
        return Collections.enumeration(sortedEntries);
    }

    /**
     * 读取压缩文件并压缩到指定目录下
     *
     * @param srcFile  源文件
     * @param destPath 解压路径
     * @param delete   是否删除源文件
     * @return 解压后路径
     */
    public static String unRarFile(String srcFile, String destPath, boolean delete) {
        try {
            File file = new File(srcFile);
            if (!file.exists()) {
                return null;
            }

            Archive archive = new Archive(file);
            List<FileHeader> headers = sortedHeaders(archive.getFileHeaders());
            String name = null;
            List<Map<String, FileHeader>> headersToBeExtracted = new ArrayList<>();
            for (FileHeader header : headers) {
                if (header.isUnicode()) {
                    name = header.getFileNameW();
                } else {
                    name = header.getFileNameString();
                }

                if (header.isDirectory()) {
                    name = name.substring(0, name.length() - 1);
                    File fileObject = new File(destPath + File.separator + name);
                    fileObject.mkdirs();
                } else {
                    headersToBeExtracted.add(Collections.singletonMap(name, header));
                }
            }

            // 开启新的线程处理文件解压
            AsyncManager.getInstance().submit(new RarExtractorWorker(headersToBeExtracted, archive, file, destPath, delete));
            return destPath;
        } catch (Exception e) {
            log.error("解压rar文件失败：{}", e.getMessage());
            return null;
        }
    }

    private static List<FileHeader> sortedHeaders(List<FileHeader> headers) {
        List<FileHeader> sortedHeaders = new ArrayList<>();
        Map<Integer, FileHeader> mapHeaders = new TreeMap<>();
        headers.forEach(header -> mapHeaders.put(header.getFileNameW().length(), header));
        for (Map.Entry<Integer, FileHeader> entry : mapHeaders.entrySet()) {
            for (FileHeader header : headers) {
                if (entry.getKey().intValue() == header.getFileNameW().length()) {
                    sortedHeaders.add(header);
                }
            }
        }
        return sortedHeaders;
    }

    /**
     * 获取顶层文件(夹)的名字
     */
    private static String getTopPath(String fullName, String separator) {
        if (fullName.startsWith(separator)) {
            fullName = fullName.substring(1);
        }
        String newName = fullName;
        if (null != fullName && fullName.contains(separator)) {
            newName = fullName.substring(0, fullName.indexOf(separator));
        }
        return newName;
    }

    /**
     * 获取最底层文件(夹)的名字
     */
    private static String getLastPath(String fullName, String separator) {
        if (fullName.endsWith(separator)) {
            fullName = fullName.substring(0, fullName.length() - 1);
        }
        String newName = fullName;
        if (null != fullName && fullName.contains(separator)) {
            newName = fullName.substring(fullName.lastIndexOf(separator) + 1);
        }
        return newName;
    }

    /**
     * 获取指定层级文件(夹)名
     *
     * @param fullName  文件（夹）完整路径
     * @param separator 分隔符
     * @param level     层级
     * @param reverse   从最上层计算
     * @return
     */
    private static String getCustomPath(String fullName, String separator, int level, boolean reverse) {
        if (reverse && fullName.startsWith(separator)) {
            fullName = fullName.substring(1);
        }
        if (!reverse && fullName.endsWith(separator)) {
            fullName = fullName.substring(0, fullName.length() - 1);
        }
        String levelName = fullName;
        do {
            int endIndex = reverse ? levelName.indexOf(separator) : levelName.lastIndexOf(separator);
            String leftPath = levelName.substring(0, endIndex == -1 ? 0 : endIndex);
            if (StringUtil.isEmpty(leftPath)) {
                return levelName;
            }
            levelName = reverse ? getTopPath(leftPath, separator) : getLastPath(leftPath, separator);
            level--;
        } while (level != 1);

        return levelName;
    }

    /**
     * 添加文件节点数据并进行排序
     *
     * @param appender   排序结果
     * @param parentName 父节点，默认""
     * @param node       文件节点信息
     */
    public static void addNodes(Map<String, FileNode> appender, String parentName, FileNode node) {
        if (appender.containsKey(parentName)) {
            appender.get(parentName).getChildList().add(node);
            Collections.sort(appender.get(parentName).getChildList(), sortComparator);
        } else {
            // 根节点
            FileNode nodeRoot = new FileNode(parentName, parentName, "", new ArrayList<>(), true);
            nodeRoot.getChildList().add(node);
            appender.put("", nodeRoot);
            appender.put(parentName, nodeRoot);
        }
    }

    private static Comparator<FileNode> sortComparator = new Comparator<FileNode>() {
        Collator cmp = Collator.getInstance(Locale.US);

        @Override
        public int compare(FileNode o1, FileNode o2) {
            // 判断两个对比对象是否是开头包含数字，如果包含数字则获取数字并按数字真正大小进行排序
            BigDecimal num1, num2;
            if (null != (num1 = isStartNumber(o1))
                    && null != (num2 = isStartNumber(o2))) {
                return num1.subtract(num2).intValue();
            }
            CollationKey c1 = cmp.getCollationKey(o1.getOriginName());
            CollationKey c2 = cmp.getCollationKey(o2.getOriginName());
            return cmp.compare(c1.getSourceString(), c2.getSourceString());
        }
    };

    private static BigDecimal isStartNumber(FileNode fileNode) {
        Matcher matcher = pattern.matcher(fileNode.getOriginName());
        if (matcher.find()) {
            return new BigDecimal(matcher.group());
        }
        return null;
    }

    /**
     * 文件节点(区分文件上下级)
     */
    @Data
    public static class FileNode {
        private String originName;
        private String fileName;
        private String parentFileName;
        private boolean directory;
        private List<FileNode> childList;

        public FileNode() {
        }

        public FileNode(String originName, String fileName, String parentFileName, List<FileNode> childList, boolean directory) {
            this.originName = originName;
            this.fileName = fileName;
            this.parentFileName = parentFileName;
            this.childList = childList;
            this.directory = directory;
        }

        @Override
        public String toString() {
            return JsonUtil.toJson(this);
        }
    }

    /**
     * Zip文件抽取线程
     */
    @Slf4j
    static class ZipExtractorWorker implements Runnable {
        private List<Map<String, ZipArchiveEntry>> entriesToBeExtracted;
        private File file;
        private ZipFile zipFile;
        private String filePath;
        private boolean delete;

        public ZipExtractorWorker(List<Map<String, ZipArchiveEntry>> entriesToBeExtracted, File file, ZipFile zipFile, String filePath, boolean delete) {
            this.entriesToBeExtracted = entriesToBeExtracted;
            this.file = file;
            this.zipFile = zipFile;
            this.filePath = filePath;
            this.delete = delete;
        }

        @Override
        public void run() {
            boolean success = true;
            try {
                for (Map<String, ZipArchiveEntry> entryMap : entriesToBeExtracted) {
                    String childName = entryMap.keySet().iterator().next();
                    ZipArchiveEntry entry = entryMap.values().iterator().next();
                    extractZipFile(childName, zipFile.getInputStream(entry));
                }
            } catch (Exception e) {
                success = false;
                log.error("读取zip压缩文件异常：{}", e.getMessage());
            }

            try {
                zipFile.close();
            } catch (IOException e) {
                log.error("关闭zip压缩文件异常：{}", e.getMessage());
            }

            if (success && delete) {
                file.deleteOnExit();
            }
        }

        /**
         * 读取压缩文件并写入到指定文件夹下
         */
        private void extractZipFile(String childName, InputStream zipFile) throws Exception {
            String outPath = filePath + File.separator + childName;
            File file = new File(outPath);
            if (!file.exists()) {
                file.createNewFile();
            }
            byte[] inByte = new byte[1024];
            int len;
            OutputStream os = new FileOutputStream(outPath);
            while ((-1 != (len = zipFile.read(inByte)))) {
                os.write(inByte, 0, len);
            }
        }
    }

    /**
     * Rar文件抽取线程
     */
    @Slf4j
    static class RarExtractorWorker implements Runnable {
        private List<Map<String, FileHeader>> headersToBeExtracted;
        private Archive archive;
        private File file;
        private String filePath;
        private boolean delete;

        public RarExtractorWorker(List<Map<String, FileHeader>> headersToBeExtracted, Archive archive, File file, String filePath, boolean delete) {
            this.headersToBeExtracted = headersToBeExtracted;
            this.archive = archive;
            this.file = file;
            this.filePath = filePath;
            this.delete = delete;
        }

        @Override
        public void run() {
            boolean success = true;
            try {
                for (Map<String, FileHeader> entryMap : headersToBeExtracted) {
                    String childName = entryMap.keySet().iterator().next();
                    extractRarFile(childName, entryMap.values().iterator().next(), archive);
                }
            } catch (Exception e) {
                success = false;
                log.error("读取rar压缩文件异常：{}", e.getMessage());
            }

            try {
                archive.close();
            } catch (IOException e) {
                log.error("关闭rar压缩文件异常：{}", e.getMessage());
            }

            if (success && delete) {
                file.deleteOnExit();
            }
        }

        /**
         * 抽取rar文件到指定目录
         */
        private void extractRarFile(String childName, FileHeader header, Archive archive) throws Exception {
            String outPath = filePath + File.separator + childName;
            File file = new File(outPath);
            if (!file.exists()) {
                file.createNewFile();
            }
            OutputStream os = new FileOutputStream(outPath);
            archive.extractFile(header, os);
        }
    }
}
